package agents;

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Timer;
import java.util.TimerTask;

import res.Util;
import res.graph.Edge;
import agents.election.GroupBallot;
import agents.election.GroupElectionJudge;
import agents.election.StateBallot;
import agents.election.StateElectionJudge;

public class TrafficLightAgent implements Runnable {

	private static final long serialVersionUID = 3747529272430944325L;
	
	private final Edge edge;
	private final Collection<TrafficLightAgent> successors = new LinkedHashSet<TrafficLightAgent>();
	private final Collection<TrafficLightAgent> competitors = new LinkedHashSet<TrafficLightAgent>();
	private final Collection<TrafficLightAgent> partners = new LinkedHashSet<TrafficLightAgent>();
	private final LinkedList<Float> antecessorsInfos = new LinkedList<Float>();
	private final Object groupLock = new Object();
	private final Object stateLock = new Object();
	
	public static final int RED_STATE = 0;
	public static final int GREEN_STATE = 1;
	private static final Timer timer = new Timer();
	
	private LinkedList<TrafficLightAgent> leaders = null;
	private GroupBallot bestGroupVoted = null;
	private StateBallot bestStateVoted = null;
	private StateBallot myLastWinnerVote = null;
	private TrafficLightAgent leader = null;
	private ReRunElection task = null;
	private int votes = 0;
	private int state = RED_STATE;
	private long lastStateChangeTime = 0;
	private boolean active = true;
	private boolean electionEnded = false;
	
	public TrafficLightAgent(Edge edge) {
		this.edge = edge;
	}
	
	public Edge getEdge() {
		return edge;
	}
	
	public int getState() {
		return state;
	}
	
	public void addCompetitor(TrafficLightAgent comp) {
		competitors.add(comp);
	}
	
	public void setCompetitors(Collection<TrafficLightAgent> comp) {
		competitors.clear();
		competitors.addAll(comp);
	}
	
	public void addPartners(TrafficLightAgent part) {
		partners.add(part);
	}
	
	public void setPartners(Collection<TrafficLightAgent> part) {
		partners.clear();
		partners.addAll(part);
	}
	
	public void addSuccessor(TrafficLightAgent succ) {
		successors.add(succ);
	}
	
	public void setSuccessors(Collection<TrafficLightAgent> succs) {
		successors.clear();
		successors.addAll(succs);
	}
	
	public final Collection<TrafficLightAgent> getSuccessors() {
		return successors;
	}

	private void addAntecessorInfo(float value) {
		synchronized (antecessorsInfos) {
			antecessorsInfos.add(value);
		}
	}
	
	private float getStateVoteInfo() {
		float rate = (float)(edge.getLoad()/edge.getWeight());
		
		synchronized (antecessorsInfos) {
			// TODO Remover cada posição da lista
		}
		return 0;
	}
	
	private void groupVote(GroupBallot g) {
		synchronized (groupLock) {
			if(GroupElectionJudge.getInstance().compare(bestGroupVoted, g) < 0) 
				bestGroupVoted = g;
			if(++votes == partners.size())
				groupLock.notify();
		}
	}
	
	private void groupElectionResult(TrafficLightAgent elected) {
		synchronized (competitors) {
			while(!electionEnded) {
				try {
					competitors.wait();
				} catch (InterruptedException e) {
					if(!active) return;
					e.printStackTrace();
				}
			}
			
			if(leaders != null)
				leaders.add(elected);
		}
	}

	private void groupElection() {
		GroupBallot myVote = new GroupBallot(this, Util.getRandom().nextLong());
		bestGroupVoted = myVote;
		
		for(TrafficLightAgent agent : partners)
			agent.groupVote(myVote);
		
		synchronized (groupLock) {
			while(votes < partners.size()) {
				try {
					groupLock.wait();
				} catch (InterruptedException e) {
					if(!active) return;
					e.printStackTrace();
				}
			}
		}
		
		if(bestGroupVoted == null || GroupElectionJudge.getInstance().compare(myVote, bestGroupVoted) > 0) {
			// Foi eleito
			leader = this;
			leaders = new LinkedList<TrafficLightAgent>();
			
			electionEnded = true;
			
			for(TrafficLightAgent agent : competitors)
				agent.groupElectionResult(this);
			
			synchronized (partners) {
				partners.notifyAll();
			}
			
			synchronized (competitors) {
				competitors.notifyAll();
			}
		} else {
			// Outro foi eleito
			leader = bestGroupVoted.getAgent();
			
			electionEnded = true;
			
			synchronized (competitors) {
				competitors.notifyAll();
			}
			
			synchronized (leader.partners) {
				while(!leader.electionEnded) {
					try {
						leader.partners.wait();
					} catch (InterruptedException e) {
						if(!active) return;
						e.printStackTrace();
					}
				}
			}
		}
	}
	
	private void stateVote(StateBallot s) {
		synchronized (stateLock) {
			if(StateElectionJudge.getInstance().compare(bestStateVoted, s) < 0) 
				bestStateVoted = s;
			if(++votes == competitors.size())
				stateLock.notify();
		}
	}
	
	private void stateElection(boolean firstTime) {
		if(!competitors.isEmpty()) {
			StateBallot myVote = null;
			long waitTime = System.currentTimeMillis() - lastStateChangeTime;
			
			if((state == RED_STATE) && !firstTime && (waitTime >= Util.Simulation.maxRedTime)) {
				if(myLastWinnerVote == null)
					myLastWinnerVote = StateBallot.getWinnerBallot(this, lastStateChangeTime);
				
				myVote = myLastWinnerVote;
			} else {
				float vote = 0;
				for(TrafficLightAgent agent : partners)
					vote += agent.getStateVoteInfo();
				
				vote += this.getStateVoteInfo();
				
				vote /= (partners.size() + 1); // Líderes votam com a média de votos do seu grupo
				
				myVote = new StateBallot(this, vote, 0);
			}
			
			bestStateVoted = myVote;
			votes = 0;
			
			for(TrafficLightAgent agent : competitors) {
				agent.stateVote(myVote);
			}
			
			synchronized (stateLock) {
				while(votes < competitors.size()) {
					try {
						stateLock.wait();
					} catch (InterruptedException e) {
						if(!active) return;
						e.printStackTrace();
					}
				}
			}
			
			if(bestStateVoted == null || StateElectionJudge.getInstance().compare(myVote, bestStateVoted) > 0) {
				// Foi eleito => fica verde
				
				for(TrafficLightAgent agent : partners)
					agent.state = GREEN_STATE;
				
				myLastWinnerVote = null;

				task.cancel();
				task = new ReRunElection(this);
				if(state == RED_STATE)
					TrafficLightAgent.timer.schedule(task, Util.Simulation.minGreenTime);
				else
					TrafficLightAgent.timer.schedule(task, Util.Simulation.refreshTime);
				
				state = GREEN_STATE;
			} else {
				// Perdeu Eleição => fica vermelho
				
				for(TrafficLightAgent agent : partners)
					agent.state = RED_STATE;
				
				if(state == GREEN_STATE)
					lastStateChangeTime = System.currentTimeMillis(); 
				
				task.cancel();
				task = new ReRunElection(this);
				TrafficLightAgent.timer.schedule(task, Util.Simulation.refreshTime);
				
				state = RED_STATE;
			}
		} else
			state = GREEN_STATE;
	}

	@Override
	public void run() {
		groupElection();
		
		if(leader == this) {
			lastStateChangeTime = System.currentTimeMillis();
			task = new ReRunElection(this);
			stateElection(true);
		}
		
		timer.scheduleAtFixedRate(new Flow(this), 0, Util.Simulation.refreshTime);
		timer.schedule(new TimerTask() {
			
			@Override
			public void run() {
				timer.purge();
			}
		}, 2*Util.Simulation.refreshTime);
	}
	
	public void stop() {
		// TODO Como interromper? 
	}

	private class ReRunElection extends TimerTask {

		private final TrafficLightAgent agent;
		
		public ReRunElection(TrafficLightAgent agent) {
			this.agent = agent;
		}
		
		@Override
		public void run() {
			agent.stateElection(false);
		} 
		
	}
	
	private class Flow extends TimerTask {

		private final TrafficLightAgent agent;
		
		public Flow(TrafficLightAgent agent) {
			this.agent = agent;
		}
		
		@Override
		public void run() {
			double load = agent.getEdge().getLoad();
			
			if(agent.state == GREEN_STATE && !agent.successors.isEmpty() && load >= Util.Simulation.flowRate) {
				int divided = Util.Simulation.flowRate / agent.successors.size();
				int forwarded = 0;
				
				for(TrafficLightAgent succ : agent.successors) {
					if(succ.getEdge().increaseLoad(divided))
						forwarded += divided; 
				}
				
				agent.getEdge().decreaseLoad(forwarded);
			}
		} 
	}
}
